Stack vs Heap
| Stack | Heap |
|---|---|
| Fast | Slower (but flexible) |
| Fixed size | Dynamic size |
| LIFO (last in, first out) | Arbitrary order |
| Known at compile time | Known at runtime |
| Automatically managed | Explicitly managed (by Rust rules) |
The Stack
The stack is a region of memory that:
- Grows and shrinks automatically
- Stores values with a known, fixed size
- Is extremely fast to access
fn main() {
let x: i32 = 5;
let y: i32 = x;
}
What happens in memory
xis pushed onto the stackygets a copy of the value- Both values exist independently
- No ownership complexity
Why copying is allowed:
i32has a fixed size- No heap allocation
- Implements
Copy
Stack Frames
Each function call gets its own stack frame:
fn foo() {
let a = 10;
}
fn main() {
foo();
}
aexists only insidefoo’s stack frame- When
fooreturns, the frame is popped - Memory is instantly reclaimed
This is why stack memory is so fast.
The Heap
The heap is used when:
- Size is unknown at compile time
- Data needs to live longer than a stack frame
- Data needs to be shared or grow dynamically
Heap allocation is:
- Slower
- More flexible
- Requires explicit rules to manage safely
Example: Heap Allocation with String
let s = String::from("hello");
Memory layout
Stack: Heap:
┌─────────────┐ ┌─────────────┐
│ s │→ │ "hello" │
│ ptr │ │ │
│ len │ │ │
│ capacity │ └─────────────┘
└─────────────┘
- The
Stringstruct lives on the stack - The actual text lives on the heap
sowns the heap allocation
Why Ownership Matters for the Heap
Problem Without Ownership
In C/C++:
char* s = malloc(5);
free(s);
printf("%s", s); // 💥 undefined behavior
Use-after-free.
Rust makes this impossible.
Move Semantics (Heap Safety)
let s1 = String::from("hello");
let s2 = s1;
- Stack data (
ptr,len,capacity) is moved - Heap allocation is NOT copied
s1is invalidated- Only one owner remains
This prevents double free.
Stack vs Heap in Function Calls
Stack-only argument
fn takes_i32(x: i32) {}
let a = 5;
takes_i32(a); // copy
- Cheap
- No ownership transfer
Heap-allocated argument
fn takes_string(s: String) {}
let s = String::from("hello");
takes_string(s); // move
- Ownership moves into the function
- Heap memory freed when function ends
Borrowing: Safe Heap Access
fn print(s: &String) {
println!("{}", s);
}
let s = String::from("hello");
print(&s);
- Stack reference passed
- Heap data is not moved
- Owner remains responsible for freeing
Borrowing exists because heap data is expensive and dangerous to duplicate.
Mutable Borrowing & Heap Writes
fn append(s: &mut String) {
s.push_str(" world");
}
let mut s = String::from("hello");
append(&mut s);
Why only one mutable borrow?
- Multiple writers to heap memory = data races
- Rust forbids this at compile time
Stack Data Referencing Heap Data
let s = String::from("hello");
let r = &s;
ris on the stackrpoints to stack data (s)spoints to heap data
Rust ensures:
rcannot outlivessfrees heap memory exactly once
Example: Dangling Pointer (Prevented)
let r: &String;
{
let s = String::from("hello");
r = &s; // ❌ compile-time error
}
Why?
s’s stack frame disappears- Heap memory would be freed
rwould dangle
Rust refuses to compile.
Stack vs Heap and Lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
- Lifetimes track stack references
- Ownership controls heap memory
- Together, they prevent dangling pointers
Performance Perspective
| Aspect | Stack | Heap |
|---|---|---|
| Allocation | Free | Expensive |
| Deallocation | Free | Expensive |
| Cache-friendly | Yes | Less |
| Ownership needed | No | Yes |
Rust:
- Prefers stack allocation by default
- Uses heap only when necessary
- Makes heap usage explicit
Common Heap Types in Rust
StringVec<T>Box<T>Rc<T>/Arc<T>
All of them:
- Own heap data
- Use ownership rules to free memory safely
Mental Model (This One Sticks)
- Stack = short-lived, fixed-size, automatic
- Heap = long-lived, dynamic, owned
- Ownership = who frees the heap
- Borrowing = temporary access
- Lifetimes = how long references are valid
Why Rust Cares So Much
Because almost all memory bugs come from:
- Heap misuse
- Aliasing + mutation
- Lifetime mismatches
Rust solves these without a garbage collector.